Skip to content

fix: Avoid showing XEMBED and SNI plugins together#418

Merged
BLumia merged 1 commit into
linuxdeepin:masterfrom
BLumia:cherry-pick-197
Jan 27, 2026
Merged

fix: Avoid showing XEMBED and SNI plugins together#418
BLumia merged 1 commit into
linuxdeepin:masterfrom
BLumia:cherry-pick-197

Conversation

@BLumia

@BLumia BLumia commented Jan 27, 2026

Copy link
Copy Markdown
Member

The trayplugin-loader will only show SNI plugin when both plugins are both available.

Bug: https://pms.uniontech.com/bug-view-270397.html
Log: Avoid showing XEMBED and SNI plugins together

此提交 cherry-pick 自 #197

Summary by Sourcery

Prevent simultaneous display of XEMBED and SNI tray icons for the same application by coordinating registration between the two protocols.

Bug Fixes:

  • Avoid showing duplicate tray icons when both XEMBED and SNI implementations are available for the same process by tracking protocol registration per PID and preferring SNI.

Enhancements:

  • Introduce a shared protocol registration map keyed by process ID and link SNI and XEMBED handlers so that SNI can request removal of overlapping XEMBED items when necessary.

The trayplugin-loader will only show SNI plugin when both plugins are
both available.

Bug: https://pms.uniontech.com/bug-view-270397.html
Log: Avoid showing XEMBED and SNI plugins together
@BLumia BLumia requested a review from tsic404 January 27, 2026 03:01
@sourcery-ai

sourcery-ai Bot commented Jan 27, 2026

Copy link
Copy Markdown

Reviewer's Guide

Ensures that for a given application PID only one tray protocol (SNI or XEmbed) is active at a time, preferring SNI when both are present, by tracking PIDs across both protocols and synchronizing add/remove events between them.

Sequence diagram for SNI taking precedence over XEmbed for same PID

sequenceDiagram
    participant SniTrayProtocol
    participant SniTrayProtocolHandler
    participant QDBusInterface as QDBusSessionBus
    participant TrayPlugin
    participant XembedProtocol
    participant AbstractTrayProtocol as RegisteredMap

    SniTrayProtocol->>SniTrayProtocol: registedItemChanged()
    SniTrayProtocol->>SniTrayProtocol: detect new SNI item
    SniTrayProtocol->>SniTrayProtocolHandler: create SniTrayProtocolHandler(id)
    SniTrayProtocol->>QDBusSessionBus: servicePid(service)
    QDBusSessionBus-->>SniTrayProtocol: pid
    SniTrayProtocol->>SniTrayProtocol: m_item2Pid[id] = pid
    SniTrayProtocol->>RegisteredMap: read registeredMap[pid]
    alt pid already registered as SNI
        SniTrayProtocol->>RegisteredMap: registeredMap[pid] = SNI
    else pid not SNI (UNKNOWN or XEMBED)
        SniTrayProtocol-->>TrayPlugin: signal removeXEmbedItemByPid(pid)
        TrayPlugin-->>XembedProtocol: onRemoveItemByPid(pid)
        XembedProtocol->>XembedProtocol: find item by pid via m_item2Pid
        XembedProtocol->>XembedProtocol: remove from m_item2Pid and m_registedItem
        XembedProtocol->>RegisteredMap: registeredMap.remove(pid)
        SniTrayProtocol->>RegisteredMap: registeredMap[pid] = SNI
    end
    SniTrayProtocol->>SniTrayProtocol: m_registedItem.insert(id, handler)
    SniTrayProtocol-->>TrayPlugin: signal trayCreated(handler)
Loading

Sequence diagram for XEmbed registration honoring SNI precedence

sequenceDiagram
    participant XembedProtocol
    participant XembedProtocolHandler
    participant UTIL as UtilHelper
    participant AbstractTrayProtocol as RegisteredMap
    participant TrayPlugin

    XembedProtocol->>XembedProtocol: onTrayIconsChanged()
    XembedProtocol->>XembedProtocol: detect new XEmbed item
    XembedProtocol->>XembedProtocolHandler: create XembedProtocolHandler(windowId)
    XembedProtocol->>UtilHelper: getWindowPid(windowId)
    UtilHelper-->>XembedProtocol: pid
    XembedProtocol->>RegisteredMap: read registeredMap[pid]
    alt pid registered as SNI
        XembedProtocol->>XembedProtocol: skip XEmbed item
    else pid not SNI (UNKNOWN or XEMBED)
        XembedProtocol->>RegisteredMap: registeredMap[pid] = XEMBED
        XembedProtocol->>XembedProtocol: m_item2Pid[windowId] = pid
        XembedProtocol->>XembedProtocol: m_registedItem.insert(windowId, handler)
        XembedProtocol-->>TrayPlugin: signal trayCreated(handler)
    end
Loading

Class diagram for coordinated SNI/XEmbed tray protocols

classDiagram
    class AbstractTrayProtocol {
        <<QObject>>
        +signal trayCreated(handler)
        -static registeredMap : QHash~uint,Protocol~
    }

    class Protocol {
        <<enum>>
        UNKNOWN = 0
        XEMBED
        SNI
    }

    class AbstractTrayProtocolHandler {
        <<QObject>>
    }

    class SniTrayProtocol {
        <<QObject>>
        +signal removeXEmbedItemByPid(pid uint)
        -m_trayManager : OrgKdeStatusNotifierWatcherInterface*
        -m_registedItem : QHash~QString,QSharedPointer~SniTrayProtocolHandler~~
        -m_item2Pid : QHash~QString,uint~
        -slot registedItemChanged()
    }

    class SniTrayProtocolHandler {
        <<QObject>>
        +static serviceAndPath(id QString) : QPair~QString,QString~
    }

    class XembedProtocol {
        <<QObject>>
        +slot onRemoveItemByPid(pid uint)
        -m_trayManager : TrayManager*
        -m_selectionManager : FdoSelectionManager*
        -m_registedItem : QHash~uint32_t,QSharedPointer~AbstractTrayProtocolHandler~~
        -m_item2Pid : QHash~uint,uint~
        -slot onTrayIconsChanged()
    }

    class XembedProtocolHandler {
        <<QObject>>
        +windowId() : uint32_t
    }

    class TrayPlugin {
        +init(proxyInter PluginProxyInterface*) void
        -slot onTrayhandlerCreatd(handler QPointer~AbstractTrayProtocolHandler~)
    }

    AbstractTrayProtocol <|-- SniTrayProtocol
    AbstractTrayProtocol <|-- XembedProtocol

    AbstractTrayProtocolHandler <|-- SniTrayProtocolHandler
    AbstractTrayProtocolHandler <|-- XembedProtocolHandler

    AbstractTrayProtocol --> Protocol

    SniTrayProtocol --> SniTrayProtocolHandler
    XembedProtocol --> XembedProtocolHandler

    TrayPlugin --> SniTrayProtocol
    TrayPlugin --> XembedProtocol

    TrayPlugin ..> SniTrayProtocol : connect trayCreated
    TrayPlugin ..> XembedProtocol : connect trayCreated
    TrayPlugin ..> XembedProtocol : connect removeXEmbedItemByPid
Loading

File-Level Changes

Change Details Files
Introduce a shared protocol registration map keyed by PID to prevent SNI and XEmbed icons from coexisting for the same app.
  • Add a Protocol enum (UNKNOWN, XEMBED, SNI) in the tray namespace for protocol identification.
  • Add a static QHash<uint, Protocol> registeredMap to AbstractTrayProtocol to track which protocol is registered per PID and make it available to all protocol implementations.
plugins/application-tray/abstracttrayprotocol.h
Update XEmbed tray handling to respect the shared PID-to-protocol map and track PIDs per item.
  • On new XEmbed tray items, resolve the window PID and skip registration if the PID is already registered as SNI, otherwise mark it as XEMBED and store the item-to-PID mapping.
  • On removal of XEmbed items, clean up the corresponding PID entry in the shared registration map and the item-to-PID map.
  • Add an onRemoveItemByPid(uint pid) slot to remove an XEmbed item by PID when requested by other protocols and wire it to m_registedItem/m_item2Pid cleanup.
plugins/application-tray/xembedprotocolhandler.cpp
plugins/application-tray/xembedprotocolhandler.h
Update SNI tray handling to own precedence and actively remove conflicting XEmbed icons for the same PID.
  • On new SNI items, determine the PID from DBus, store it in an item-to-PID map, and if the PID is not already registered as SNI emit a signal to remove any XEmbed item for that PID before marking it as SNI in the shared map.
  • On removal of SNI items, remove the corresponding PID from both the item-to-PID map and the shared registration map.
  • Declare the removeXEmbedItemByPid(uint pid) signal and add per-item PID tracking in SniTrayProtocol.
plugins/application-tray/sniprotocolhandler.cpp
plugins/application-tray/sniprotocolhandler.h
Wire SNI and XEmbed protocols so SNI can request XEmbed cleanup for a given PID.
  • Connect SniTrayProtocol::removeXEmbedItemByPid to XembedProtocol::onRemoveItemByPid in TrayPlugin::init so SNI taking precedence will trigger removal of any XEmbed tray entry for the same PID.
  • Keep existing trayCreated signal wiring unchanged aside from the new connection.
plugins/application-tray/trayplugin.cpp

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

这份代码 diff 主要实现了在系统托盘插件中,通过 PID(进程 ID)来协调 SNI (StatusNotifierItem) 和 XEmbed 两种托盘协议的互斥逻辑。即:如果一个进程同时提供了 SNI 和 XEmbed 托盘图标,优先使用 SNI 协议,并移除 XEmbed 协议下的图标。

以下是对代码的语法逻辑、代码质量、代码性能和代码安全的详细审查及改进意见:

1. 语法逻辑

  • 全局静态变量的线程安全性

    • 问题:在 abstracttrayprotocol.h 中定义了 inline static QHash<uint, enum Protocol> registeredMap;。这是一个静态成员变量,被 SniTrayProtocolXembedProtocol 共享。由于这两个协议处理可能运行在不同的线程(或者通过 Qt 的信号槽机制跨线程调用),直接读写这个全局的 QHash非线程安全的。
    • 风险:如果 SNI 线程正在写入 registeredMap,而 XEmbed 线程正在读取或移除,可能会导致哈希表内部数据损坏或程序崩溃。
    • 改进建议:必须对 registeredMap 的访问加锁。建议引入 QMutex 或使用 QReadWriteLock 来保护这个共享资源。
  • PID 获取失败的处理

    • 问题:在 sniprotocolhandler.cpp 中:
      uint pid = QDBusConnection::sessionBus().interface()->servicePid(...).value();
      如果 DBus 调用失败(例如服务已断开),value() 可能会返回默认值 0 或无效数据。代码直接将这个 pid 存入 registeredMap
    • 风险:PID 为 0 或无效时,会导致后续逻辑判断错误(例如误删其他托盘项)。
    • 改进建议:在获取 PID 后增加有效性检查,如果获取失败或为 0,应记录日志并跳过该条目的注册或移除逻辑。
  • PID 映射的生命周期管理

    • 问题:在 sniprotocolhandler.cpp 的移除逻辑中:
      registeredMap.remove(pid);
      这里假设一个进程只会注册一个托盘图标。如果同一个进程注册了多个 SNI 托盘图标,当其中一个图标被移除时,直接 remove(pid) 会导致该 PID 在 registeredMap 中的记录被清空。如果此时该进程还有其他 SNI 图标,状态就会变成 UNKNOWN,这可能导致 XEmbed 图标错误地复活。
    • 改进建议registeredMap 的移除逻辑需要更严谨。建议使用引用计数(QHash<uint, int>),记录每个 PID 对应的图标数量。只有当计数归零时,才从 registeredMap 中移除该 PID。

2. 代码质量

  • 拼写错误

    • m_registedItem 应该拼写为 m_registeredItem(double 't')。虽然在当前 diff 中保持了一致性,但不符合标准英语拼写,建议重构修正。
  • 命名规范

    • enum Protocol 中的枚举值 UNKNOWN, XEMBED, SNI 最好加上类名前缀或使用 enum class,以避免命名空间污染。例如:
      enum class Protocol {
          Unknown = 0,
          XEmbed,
          SNI
      };
  • 代码重复

    • SniTrayProtocolXembedProtocol 中都有维护 m_item2Pid 以及移除时清理 registeredMap 的逻辑。这部分逻辑非常相似。
    • 改进建议:可以将 PID 管理的逻辑上移到基类 AbstractTrayProtocol 中,或者提取出一个辅助类/方法,减少重复代码。
  • 查找逻辑的性能

    • xembedprotocolhandler.cpponRemoveItemByPid 中:
      auto it = std::find_if(m_registedItem.keys().begin(), ..., ...);
      这里调用了 m_registedItem.keys(),它会创建一个临时的 QList 副本。如果托盘图标数量很多,这会造成不必要的内存分配和拷贝开销。
    • 改进建议:直接遍历 m_registedItem(基于 STL 风格的迭代器或 Java 风格迭代器),或者直接利用 m_item2Pid 的反向映射(如果有的话)。由于已经有了 m_item2Pid,其实可以通过遍历 m_item2Pid 来找到对应的 Key,但这依然不够高效。最好的方式是维护一个 QHash<uint, QString> (pid -> key) 的反向映射,实现 O(1) 查找。

3. 代码性能

  • DBUS 调用开销

    • 在 SNI 协议中,每个新图标注册时都会发起一次 DBus 调用 (servicePid)。如果应用短时间内大量注册/注销图标,会造成频繁的 IPC 开销。
    • 现状:这是业务逻辑必须的,难以避免,但应确保调用是异步的或非阻塞的(Qt DBus 通常处理得较好)。
  • 线性查找

    • 如上所述,onRemoveItemByPid 使用了 std::find_if 进行线性查找。最坏情况复杂度为 O(N)。
    • 改进建议:引入 QHash<uint, QString> m_pid2Item; 作为反向映射。
      • 添加图标时:m_pid2Item[pid] = key;
      • 删除图标时:QString key = m_pid2Item.take(pid); if (!key.isEmpty()) { ... }
      • 这样可以将查找复杂度降低到 O(1)。

4. 代码安全

  • 信号槽连接类型
    • TrayPlugin::init 中连接了 removeXEmbedItemByPid 信号:
      connect(sniProtocol, ..., xembedProtocol, ...);
    • 如果 sniProtocolxembedProtocol 所在的线程不同(例如 TrayPlugin 将它们移动到了子线程),默认的 Qt::AutoConnection 是安全的(会排队执行)。但如果它们在同一线程,这就是直接调用。
    • 潜在风险:在 registedItemChanged 中,先修改了 registeredMap,然后 emit removeXEmbedItemByPid。这会导致在同一个调用栈中,SNI 正在修改数据,XEmbed 的槽函数也在读取/修改数据。这进一步加剧了线程安全中提到的竞态条件风险,即便是在单线程环境下,这种重入式的逻辑也容易导致状态不一致。
    • 改进建议:确保对共享数据的操作是原子的,或者使用 QMutexLocker 互斥锁保护整个临界区。

总结与改进代码示例

针对上述问题,这里提供一个关于基类改进和线程安全的建议代码片段:

abstracttrayprotocol.h 修改建议:

#include <QMutex>
#include <QReadWriteLock>

// ... namespace tray ...

enum class Protocol { // 使用 enum class
    Unknown = 0,
    XEmbed,
    SNI
};

class AbstractTrayProtocol : public QObject
{
    Q_OBJECT
public:
    // ...
    
protected:
    // 使用读写锁,允许多读单写
    static QReadWriteLock m_mapLock;
    inline static QHash<uint, Protocol> registeredMap;
    
    // 辅助函数:安全地检查和更新协议类型
    static bool tryRegisterProtocol(uint pid, Protocol type) {
        QWriteLocker locker(&m_mapLock);
        if (registeredMap.value(pid, Protocol::Unknown) == Protocol::Unknown || 
            registeredMap.value(pid, Protocol::Unknown) == type) {
            registeredMap[pid] = type;
            return true;
        }
        return false;
    }
    
    static void unregisterProtocol(uint pid) {
        QWriteLocker locker(&m_mapLock);
        registeredMap.remove(pid);
    }
    
    static Protocol getProtocol(uint pid) {
        QReadLocker locker(&m_mapLock);
        return registeredMap.value(pid, Protocol::Unknown);
    }
};

sniprotocolhandler.cpp 逻辑改进建议:

// 在注册项时
uint pid = ...;
if (pid > 0) { // 检查有效性
    m_item2Pid[currentRegistedItem] = pid;
    
    // 检查是否已有其他协议注册
    Protocol currentProto = AbstractTrayProtocol::getProtocol(pid);
    if (currentProto != Protocol::Unknown && currentProto != Protocol::SNI) {
        // 只有当确实存在冲突时才发送信号
        emit removeXEmbedItemByPid(pid);
    }
    // 更新当前协议
    AbstractTrayProtocol::tryRegisterProtocol(pid, Protocol::SNI);
}

// 在移除项时
if (auto value = m_registedItem.value(alreadyRegistedItem, nullptr)) {
    uint pid = m_item2Pid.take(alreadyRegistedItem); // take 自动移除
    if (pid > 0) {
        // 注意:这里不应直接 remove,因为同一进程可能有多个图标
        // 实际项目中应引入引用计数逻辑
        AbstractTrayProtocol::unregisterProtocol(pid); 
    }
    m_registedItem.remove(alreadyRegistedItem);
}

xembedprotocolhandler.h 性能改进建议:

// 增加 pid 到 key 的反向映射,优化查找
QHash<uint, uint> m_item2Pid; // key(windowId) -> pid
QHash<uint, uint> m_pid2Item; // pid -> key(windowId)

xembedprotocolhandler.cpp 查找逻辑优化:

void XembedProtocol::onRemoveItemByPid(uint pid)
{
    // 使用 O(1) 反向查找
    if (m_pid2Item.contains(pid)) {
        uint key = m_pid2Item.take(pid);
        m_item2Pid.remove(key);
        m_registedItem.remove(key);
    }
}

这些改进将显著提高代码的健壮性、安全性和执行效率。

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In several places you use QHash::operator[] for lookups (e.g. m_item2Pid[alreadyRegistedItem], m_item2Pid[id], registeredMap[pid]), which will insert default entries on missing keys; consider using value() or checking contains() first to avoid unintended insertions and incorrect registeredMap/m_item2Pid state.
  • In XembedProtocol::onRemoveItemByPid, m_registedItem.keys() is recomputed multiple times inside std::find_if, and you index m_item2Pid without checking the key exists; caching the keys in a local variable and guarding m_item2Pid access with contains() would make this safer and more efficient.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In several places you use `QHash::operator[]` for lookups (e.g. `m_item2Pid[alreadyRegistedItem]`, `m_item2Pid[id]`, `registeredMap[pid]`), which will insert default entries on missing keys; consider using `value()` or checking `contains()` first to avoid unintended insertions and incorrect `registeredMap`/`m_item2Pid` state.
- In `XembedProtocol::onRemoveItemByPid`, `m_registedItem.keys()` is recomputed multiple times inside `std::find_if`, and you index `m_item2Pid` without checking the key exists; caching the keys in a local variable and guarding `m_item2Pid` access with `contains()` would make this safer and more efficient.

## Individual Comments

### Comment 1
<location> `plugins/application-tray/sniprotocolhandler.cpp:94-96` </location>
<code_context>
     for (auto alreadyRegistedItem : m_registedItem.keys()) {
         if (!currentRegistedItems.contains(alreadyRegistedItem)) {
             if (auto value = m_registedItem.value(alreadyRegistedItem, nullptr)) {
+                uint pid = m_item2Pid[alreadyRegistedItem];
+                m_item2Pid.remove(alreadyRegistedItem);
+                registeredMap.remove(pid);
                 m_registedItem.remove(alreadyRegistedItem);
             }
</code_context>

<issue_to_address>
**issue (bug_risk):** Avoid using operator[] for lookups in m_item2Pid to prevent unintended insertions and desynchronization

Using `m_item2Pid[alreadyRegistedItem]` here will insert a default entry if the key is missing. If `m_item2Pid` and `m_registedItem` are ever out of sync, you’ll silently create an entry with pid 0 and then call `registeredMap.remove(0)`. Prefer `m_item2Pid.value(alreadyRegistedItem, 0)` with a `contains` check, or guard the block with `if (m_item2Pid.contains(alreadyRegistedItem))` to avoid unintended insertions.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +94 to +96
uint pid = m_item2Pid[alreadyRegistedItem];
m_item2Pid.remove(alreadyRegistedItem);
registeredMap.remove(pid);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Avoid using operator[] for lookups in m_item2Pid to prevent unintended insertions and desynchronization

Using m_item2Pid[alreadyRegistedItem] here will insert a default entry if the key is missing. If m_item2Pid and m_registedItem are ever out of sync, you’ll silently create an entry with pid 0 and then call registeredMap.remove(0). Prefer m_item2Pid.value(alreadyRegistedItem, 0) with a contains check, or guard the block with if (m_item2Pid.contains(alreadyRegistedItem)) to avoid unintended insertions.

@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: BLumia, tsic404

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@BLumia BLumia merged commit f4a5d38 into linuxdeepin:master Jan 27, 2026
9 of 10 checks passed
@BLumia BLumia deleted the cherry-pick-197 branch January 27, 2026 03:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants